package com.freewater.library.lending.book.application;

import com.freewater.library.catalogue.model.Book;
import com.freewater.library.catalogue.model.BookRepository;
import com.freewater.library.common.domain.BaseAggregateRoot;
import com.freewater.library.common.domain.DomainEvent;
import com.freewater.library.common.domain.DomainException;
import com.freewater.library.common.infrastructure.event.DomainEventDispatcher;
import com.freewater.library.lending.book.application.command.*;
import com.freewater.library.lending.book.model.BookInstance;
import com.freewater.library.lending.book.model.BookInstanceFactory;
import com.freewater.library.lending.book.model.BookInstanceRepository;
import com.freewater.library.lending.book.model.BookLendingService;
import com.freewater.library.lending.reader.model.Reader;
import com.freewater.library.lending.reader.model.ReaderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Validate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Slf4j
@Component
@RequiredArgsConstructor
public class BookInstanceCommandService {

    private final BookInstanceFactory bookInstanceFactory;
    private final BookInstanceRepository bookInstanceRepository;
    private final DomainEventDispatcher domainEventDispatcher;
    private final ReaderRepository readerRepository;
    private final BookLendingService bookLendingService;
    private final BookRepository bookRepository;

    @Transactional
    public void createBook(CreateBookInstanceCommand command) {
        String isbn = command.getIsbn();
        String qrCode = command.getQrCode();
        BookInstance bookInstance = bookInstanceFactory.createBookInstance(isbn, qrCode);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void putOnBook(PutOnBookInstanceCommand command) {
        String qrCode = command.getQrCode();
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        bookInstance.putOn();
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void takenOffBook(TakenOffBookInstanceCommand command) {
        String qrCode = command.getQrCode();
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        bookInstance.takeOff();
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void reserveBook(ReserveBookInstanceCommand command) {
        Long readerId = command.getReaderId();
        String isbn = command.getIsbn();
        Book book = this.requireBook(isbn);
        Reader reader = this.requireReader(readerId);
        BookInstance bookInstance = this.bookLendingService.reserveBook(book, reader);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void cancelReserveBook(CancelReserveBookInstanceCommand command) {
        Long readerId = command.getReaderId();
        String qrCode = command.getQrCode();
        String reason = command.getReason();
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        Reader reader = this.requireReader(readerId);
        bookInstance.cancelReserve(reason);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void reserveOverdue(BookInstance bookInstance, LocalDateTime nowTime) {
        Validate.notNull(bookInstance, "预定书不能为空");
        Validate.notNull(nowTime, "当前时间不能为空");
        bookInstance.reservedOverdue(nowTime);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void checkOutBook(CheckOutBookInstanceCommand command) {
        Long readerId = command.getReaderId();
        String qrCode = command.getQrCode();
        Reader reader = this.requireReader(readerId);
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        this.bookLendingService.checkOut(bookInstance, reader);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void checkOutBookOverdue(BookInstance bookInstance, LocalDateTime nowTime) {
        Validate.notNull(bookInstance, "借出书不能为空");
        Validate.notNull(nowTime, "当前时间不能为空");
        bookInstance.checkedOutOverdue(nowTime);
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void checkOutBookLost(CheckOutBookInstanceLostCommand command) {
        Long readerId = command.getReaderId();
        String qrCode = command.getQrCode();
        Reader reader = this.requireReader(readerId);
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        bookInstance.submitLost();
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    @Transactional
    public void returnBackBook(ReturnBackBookInstanceCommand command) {
        Long readerId = command.getReaderId();
        String qrCode = command.getQrCode();
        Reader reader = this.requireReader(readerId);
        BookInstance bookInstance = this.requireBookInstance(qrCode);
        bookInstance.returnBack();
        this.bookInstanceRepository.save(bookInstance);
        this.dispatchEvents(bookInstance);
    }

    private Book requireBook(String isbn) {
        Optional<Book> bookOpt = this.bookRepository.findById(isbn);
        return bookOpt.orElseThrow(() -> new DomainException("无效的书"));
    }

    private BookInstance requireBookInstance(String qrCode) {
        Optional<BookInstance> bookOpt = this.bookInstanceRepository.findById(qrCode);
        return bookOpt.orElseThrow(() -> new DomainException("无效的书"));
    }

    private Reader requireReader(Long readerId) {
        Optional<Reader> readerOpt = this.readerRepository.findById(readerId);
        return readerOpt.orElseThrow(() -> new DomainException("无效的读者"));
    }

    private void dispatchEvents(BaseAggregateRoot agr) {
        for (DomainEvent event : agr.getEvents()) {
            this.domainEventDispatcher.dispatch(event);
        }
        agr.clearEvents();
    }
}
